﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Timers;

namespace MessageHandler
{
	public class Controller
	{
		Persistence persistence;
		Connection connection;
		Configuration configuration;

		Serializer serializer;

		Dictionary<string, UserInfo> registeredUsers;
		List<ClientConnection> clientConnections = new List<ClientConnection>();

		Timer timer;

		public Controller(Configuration config)
		{
			persistence = new Persistence();
			registeredUsers = persistence.Load();
			Init(config);

		}

		private void Init(Configuration config)
		{
			try
			{
				this.serializer = new Serializer(typeof(Message));
				this.configuration = config;
				InitConnection();
				InitTimer();
			}
			catch (ChatException err)
			{
				OnErrorOccured(err);
			}
		}

		private void InitTimer()
		{
			timer = new Timer(1000);
			timer.Elapsed += new ElapsedEventHandler(timer_Elapsed);
			timer.Start();
		}

		private void InitConnection()
		{
			this.connection = new Connection(new HandlerAdapter(this));
			try
			{
				this.connection.Connect(configuration);
			}
			catch (ChatException err)
			{
				OnErrorOccured(err);
			}
		}

		void timer_Elapsed(object sender, ElapsedEventArgs e)
		{
			List<ClientConnection> tmpConnections = new List<ClientConnection>();
			foreach (ClientConnection conn in clientConnections)
			{
				if (!conn.Socket.Connected)
				{
					tmpConnections.Add(conn);
				}
			}
			if (tmpConnections.Count > 0)
			{
				foreach (ClientConnection conn in tmpConnections)
				{
					clientConnections.Remove(conn);
					ChatException ex = new ChatException(
						string.Format("Verbindung von {0} wurde beendet", conn.UserInfo.ToString()),
						"Automatische Trennung von Verbindungen");
					OnErrorOccured(ex);
				}
				SendUserListUpdate();
			}
		}

		#region SendMessages

		private void SendPrivateMessage(Message incomingMessage)
		{
			PrivateMessageType type = (PrivateMessageType)incomingMessage.Item;
			List<UserInfo> userInfos = new List<UserInfo>();
			userInfos.Add(type.from);
			userInfos.AddRange(type.to);
			SendAsXml(incomingMessage, GetSocketList(userInfos));
		}

		private void SendRegisterMessage(Message incomingMessage, Socket connectedSocket)
		{
			List<Socket> sockets = new List<Socket>();
			sockets.Add(connectedSocket);
			SendAsXml(incomingMessage, sockets);
		}

		private void SendLogin(UserInfo newUser, ClientConnection clientConnection)
		{
			Message message = new Message();
			message.messageType = MessageType.USERLOGIN;
			message.Item = new UserLoginMessageType() { newUser = registeredUsers[newUser.username] };

			List<Socket> sockets = new List<Socket>();
			sockets.Add(clientConnection.Socket);
			SendAsXml(message, sockets);
		}

		private void SendLogout(Message incomingMessage, Socket connectedSocket)
		{
			UserLogoutMessageType type = (UserLogoutMessageType)incomingMessage.Item;
			List<Socket> sockets = new List<Socket>();
			sockets.Add(connectedSocket);

			SendAsXml(incomingMessage, sockets);
			connection.Close(connectedSocket);
		}

		private void SendProfileUpdate(UserInfo userInfo, Socket connectedSocket)
		{
			Message message = new Message();
			message.messageType = MessageType.PROFILEUPDATE;
			message.Item = new ProfileUpdateMessageType() { userInfo = registeredUsers[userInfo.username] };
			List<Socket> sockets = new List<Socket>();
			sockets.Add(connectedSocket);
			SendAsXml(message, sockets);
		}

		private void SendUserListUpdate()
		{
			foreach (ClientConnection conn in clientConnections)
			{
				SendUserListUpdateForUser(conn.UserInfo, conn.Socket);
			}
		}

		private void SendUserListUpdateForUser(UserInfo userInfo, Socket connectedSocket)
		{
			Message message = new Message();
			message.messageType = MessageType.USERLISTUPDATE;
			userInfo = registeredUsers[userInfo.username];
			message.Item = new UserListUpdateMessageType() { currentUser = userInfo, currentUserList = GetOnlineContactsForUser(userInfo) };
			List<Socket> sockets = new List<Socket>();
			sockets.Add(connectedSocket);
			SendAsXml(message, sockets);
		}

		private UserInfo[] GetOnlineContactsForUser(UserInfo currentUser)
		{
			if (currentUser.contacts == null)
			{
				return new UserInfo[0];
			}
			List<UserInfo> users = new List<UserInfo>();
			foreach (string user in currentUser.contacts)
			{
				foreach (ClientConnection con in clientConnections)
				{
					if (con.UserInfo.username == user)
					{
						users.Add(registeredUsers[user]);
					}
				}
			}
			return users.ToArray();
		}

		private void ForwardStartFileTransferMessage(Message message)
		{
			StartFileTransferMessageType type = (StartFileTransferMessageType)message.Item;
			SendAsXml(message, GetSocketList(new List<UserInfo>(type.to)));
		}

		private void AnswerStartFileTransferMessage(Message message)
		{
			StartFileTransferMessageType type = (StartFileTransferMessageType)message.Item;
			List<UserInfo> user = new List<UserInfo>();
			user.Add(type.from);
			SendAsXml(message, GetSocketList(user));
		}

		private void SendFileTransferMessage(Message incomingMessage, Socket connectedSocket)
		{
			FileTransferMessageType type = (FileTransferMessageType)incomingMessage.Item;
			List<UserInfo> userInfos = new List<UserInfo>();
			userInfos.Add(type.recipient);
			SendAsXml(incomingMessage, GetSocketList(userInfos));
		}

		private void SendContactSearchMessage(string searchText, UserInfo[] users, Socket connectedSocket)
		{
			Message message = new Message();
			message.messageType = MessageType.CONTACTSEARCH;
			message.Item = new ContactSearchMessageType() { searchResultList = users };
			message.text = searchText;
			List<Socket> sockets = new List<Socket>();
			sockets.Add(connectedSocket);
			SendAsXml(message, sockets);
		}

		private void SendNewErrorMessage(UserInfo[] userInfos, string errorText, string ipAddress, Socket connectedSocket)
		{
			Message message = new Message();
			message.messageType = MessageType.ERROR;
			message.Item = new ErrorMessageType()
			{
				from = new UserInfo() { ip = ipAddress, port = configuration.Port, username = "Server", password = "" },
				to = userInfos
			};
			message.text = errorText;
			List<Socket> sockets = new List<Socket>();
			sockets.Add(connectedSocket);
			SendAsXml(message, sockets);
		}

		private void ForwardErrorMessage(Message incomingMessage, Socket connectedSocket)
		{
			ErrorMessageType type = (ErrorMessageType)incomingMessage.Item;
			List<UserInfo> users = new List<UserInfo>(type.to);
			SendAsXml(incomingMessage, GetSocketList(users));
		}

		private void SendAsXml(Message message, List<Socket> socketList)
		{
			MemoryStream stream = new MemoryStream();
			serializer.SerializeObject(message, ref stream);
			try
			{
				connection.Send(stream.ToArray(), socketList);
			}
			catch (ChatException err)
			{
				err.Caption = Enum.GetName(typeof(MessageType), message.messageType);
				OnErrorOccured(err);
			}
		}

		private List<Socket> GetSocketList(List<UserInfo> userInfos)
		{
			var socketList = from cc in clientConnections
							 where userInfos.Contains(cc.UserInfo)
							 select cc.Socket;
			return socketList.ToList<Socket>();
		}
		#endregion

		#region Handle Messages

		internal void HandleIncomingMessage(byte[] buffer, Socket connectedSocket)
		{
			try
			{
				Message message = (Message)serializer.DeserializeObject(new MemoryStream(buffer));

				switch (message.messageType)
				{
					case MessageType.PRIVATE:
						SendPrivateMessage(message);
						break;
					case MessageType.USERLISTUPDATE:
						SendUserListUpdateForUser(((UserListUpdateMessageType)message.Item).currentUser, connectedSocket);
						break;
					case MessageType.CONTACTADD:
						HandleContactAdd(message, connectedSocket);
						break;
					case MessageType.CONTACTSEARCH:
						HandleContactSearch(message, connectedSocket);
						break;
					case MessageType.PROFILEUPDATE:
						HandleProfileUpdate(message, connectedSocket);
						break;
					case MessageType.USERLOGIN:
						HandleLogin(message, connectedSocket);
						break;
					case MessageType.USERLOGOUT:
						HandleLogout(message, connectedSocket);
						break;
					case MessageType.STARTFILETRANSFER:
						HandleStartFileTransfer(message, connectedSocket);
						break;
					case MessageType.FILETRANSFER:
						SendFileTransferMessage(message, connectedSocket);
						break;
					case MessageType.ERROR:
						ForwardErrorMessage(message, connectedSocket);
						break;
					case MessageType.REGISTER:
						HandleRegister(message, connectedSocket);
						break;
					default:
						break;
				}
				OnMessageReceived(message);
			}
			catch (ChatException err)
			{
				OnErrorOccured(err);
			}
		}

		private void HandleRegister(Message message, Socket connectedSocket)
		{
			RegisterMessageType type = (RegisterMessageType)message.Item;
			UserInfo userInfo = type.newUser;
			if (registeredUsers.ContainsKey(userInfo.username))
			{
				UserInfo[] users = { userInfo };
				SendNewErrorMessage(users, "Username ist bereits vergeben.", userInfo.ip, connectedSocket);
				return;
			}

			if (userInfo.contacts == null)
			{
				userInfo.contacts = new string[0];
			}
			registeredUsers.Add(userInfo.username, userInfo);
			SendRegisterMessage(message, connectedSocket);
		}

		private void HandleLogout(Message message, Socket connectedSocket)
		{
			UserLogoutMessageType type = (UserLogoutMessageType)message.Item;
			ClientConnection conn = new ClientConnection();
			conn.Socket = connectedSocket;
			conn.UserInfo = type.loggedOutUser;
			clientConnections.Remove(conn);
			SendLogout(message, connectedSocket);
			SendUserListUpdate();
		}

		private void HandleLogin(Message message, Socket connectedSocket)
		{
			UserLoginMessageType type = (UserLoginMessageType)message.Item;
			if (CheckLogin(type.newUser))
			{
				ClientConnection conn = new ClientConnection();
				conn.Socket = connectedSocket;
				conn.UserInfo = registeredUsers[type.newUser.username];
				if (clientConnections.Contains(conn))
				{
					UserInfo[] users = { type.newUser };
					SendNewErrorMessage(users, "Sie sind bereits eingeloggt.", type.newUser.ip, connectedSocket);
				}
				else
				{
					clientConnections.Add(conn);
					SendLogin(type.newUser, conn);
					SendUserListUpdate();
				}
			}
			else
			{
				UserInfo[] users = { type.newUser };
				SendNewErrorMessage(users, "Logindaten nicht korrekt.", type.newUser.ip, connectedSocket);
			}
		}

		private bool CheckLogin(UserInfo userInfo)
		{
			if (registeredUsers.ContainsKey(userInfo.username))
			{
				return registeredUsers[userInfo.username].password == userInfo.password;
			}
			return false;
		}

		private string CheckUserName(string name)
		{
			string username = name;
			foreach (ClientConnection conn in clientConnections)
			{
				if (conn.UserInfo.username == username)
				{
					string[] tmpNameParts = username.Split('_');
					if (tmpNameParts.Length > 1)
					{
						int count;
						if (int.TryParse(tmpNameParts[tmpNameParts.Length - 1], out count))
						{
							string tmpUsername = string.Empty;
							for (int i = 0; i < tmpNameParts.Length - 1; i++)
							{
								tmpUsername += tmpNameParts[i] + "_";
							}
							username = CheckUserName(tmpUsername + (1 + count));
						}
						else
						{
							username = CheckUserName(username + "_1");
						}
					}
					else
					{
						username = CheckUserName(username + "_1");
					}
				}
			}
			return username;
		}

		private void HandleContactAdd(Message message, Socket connectedSocket)
		{
			ContactAddMessageType type = (ContactAddMessageType)message.Item;

			if (type.fromUser.Equals(type.toUser))
			{
				UserInfo[] users = { type.toUser };
				SendNewErrorMessage(users, "Sie können sich nicht selbst als Kontakt hinzufügen.", type.toUser.ip, connectedSocket);
				return;
			}

			foreach (var conn in clientConnections)
			{
				if (conn.Socket.Equals(connectedSocket))
				{
					if (registeredUsers[type.fromUser.username].contacts.Contains(type.toUser.username))
					{
						SendNewErrorMessage(new UserInfo[] { type.toUser }, "Dieser Nutzer ist bereits in Ihrer Kontaktliste.", type.fromUser.ip, connectedSocket);
						return;
					}

					var sockets = new List<Socket>();
					if (conn.UserInfo.Equals(type.fromUser))
					{
						sockets.Add(clientConnections.Find(x => x.UserInfo.Equals(registeredUsers[type.toUser.username])).Socket);
						SendAsXml(message, sockets);
					}
					else if (conn.UserInfo.Equals(type.toUser))
					{
						AddContacts(type.fromUser, type.toUser);
						sockets.Add(clientConnections.Find(x => x.UserInfo.Equals(registeredUsers[type.fromUser.username])).Socket);
						SendAsXml(message, sockets);
						SendUserListUpdateForUser(registeredUsers[type.fromUser.username],
							clientConnections.Find(x => x.UserInfo.Equals(registeredUsers[type.fromUser.username])).Socket);
						SendUserListUpdateForUser(registeredUsers[type.toUser.username],
							clientConnections.Find(x => x.UserInfo.Equals(registeredUsers[type.toUser.username])).Socket);
					}
				}
			}
		}

		private void AddContacts(UserInfo user1, UserInfo user2)
		{
			string[] contacts1 = new string[registeredUsers[user1.username].contacts.Length + 1];
			Array.Copy(registeredUsers[user1.username].contacts, contacts1, contacts1.Length - 1);
			contacts1[contacts1.Length - 1] = user2.username;
			registeredUsers[user1.username].contacts = contacts1;

			string[] contacts2 = new string[registeredUsers[user2.username].contacts.Length + 1];
			Array.Copy(registeredUsers[user2.username].contacts, contacts2, contacts2.Length - 1);
			contacts2[contacts2.Length - 1] = user1.username;
			registeredUsers[user2.username].contacts = contacts2;
		}

		private void HandleContactSearch(Message message, Socket connectedSocket)
		{
			List<UserInfo> users = new List<UserInfo>();
			foreach (ClientConnection conn in clientConnections)
			{
				if (conn.UserInfo.username.ToLower().Contains(message.text.ToLower()))
				{
					users.Add(conn.UserInfo);
				}
			}

			SendContactSearchMessage(message.text, users.ToArray(), connectedSocket);
		}

		private void HandleStartFileTransfer(Message message, Socket connectedSocket)
		{
			StartFileTransferMessageType type = (StartFileTransferMessageType)message.Item;
			if (ConnectedSocketEqualsFrom(type.from, connectedSocket))
			{
				ForwardStartFileTransferMessage(message);
			}
			else
			{
				AnswerStartFileTransferMessage(message);
			}
		}

		private void HandleProfileUpdate(Message message, Socket connectedSocket)
		{
			ProfileUpdateMessageType type = (ProfileUpdateMessageType)message.Item;
			if (ConnectedSocketEqualsFrom(type.userInfo, connectedSocket))
			{
				ChangeProfile(type.userInfo);
			}
			else
			{
				SendProfileUpdate(type.userInfo, connectedSocket);
			}
		}

		private bool ConnectedSocketEqualsFrom(UserInfo userInfo, Socket connectedSocket)
		{
			return clientConnections.Find(x => x.UserInfo.Equals(userInfo)).Socket.Equals(connectedSocket);
		}

		private void ChangeProfile(UserInfo userInfo)
		{
			UserInfo ui = registeredUsers[userInfo.username];
			ui.birthday = userInfo.birthday;
			ui.birthdaySpecified = true;
			ui.firstname = userInfo.firstname;
			ui.lastname = userInfo.lastname;
			ui.gender = userInfo.gender;
			ui.genderSpecified = true;
		}

		internal void HandleError(ChatException err)
		{
			OnErrorOccured(err);
		}

		#endregion

		#region EventHandler

		#region MessageReceived

		public delegate void MessageReceivedEventHandler(object sender, MessageReceivedEventArgs e);

		public event MessageReceivedEventHandler MessageReceived;

		protected void OnMessageReceived(Message message)
		{
			if (MessageReceived != null)
			{
				MessageReceivedEventArgs args = new MessageReceivedEventArgs();
				args.Message = message;
				MessageReceived(this, args);
			}
		}
		#endregion

		#region ErrorOccured

		public delegate void ErrorOccuredEventHandler(object sender, ErrorOccuredEventArgs e);

		public event ErrorOccuredEventHandler ErrorOccured;

		protected void OnErrorOccured(ChatException error)
		{
			if (ErrorOccured != null)
			{
				ErrorOccuredEventArgs args = new ErrorOccuredEventArgs();
				args.Error = error;
				ErrorOccured(this, args);
			}
		}
		#endregion

		#endregion

		public void Close()
		{
			persistence.Persist(registeredUsers);

			foreach (ClientConnection conn in clientConnections)
			{
				connection.Close(conn.Socket);
			}
		}
	}
}
